﻿using System;
using System.Text.RegularExpressions;
using System.Net;
using System.IO;
using System.Text;
using System.Data.SQLite;
using System.Data;
using System.Configuration;
using System.Security.Cryptography;
using System.Reflection;

namespace WebSiteUpdateChecker {
    
    
    public partial class WebPageDataSet
    {

        #region 共通

        /// <summary>データベース接続オブジェクト。</summary>
        private static SQLiteConnection connection;

        /// <summary>
        /// DBの接続。
        /// </summary>
        public void OpenConnection()
        {
            // DBが未作成の場合は作成
            if (connection == null)
            {
                connection = new SQLiteConnection(Properties.Settings.Default.ConnectionString);
            }
            // DBが閉じている場合は開く
            if (connection.State == ConnectionState.Closed)
            {
                connection.Open();
            }
            // 未作成のテーブルを作成
            CreateDbTable();
        }

        /// <summary>
        /// DBの切断。
        /// </summary>
        public void CloseConnection()
        {
            if (connection != null)
            {
                connection.Close();
            }
        }

        /// <summary>
        /// データベースのテーブルを作成する。
        /// </summary>
        private void CreateDbTable()
        {
            
            using (SQLiteTransaction tran = connection.BeginTransaction())
            using (SQLiteCommand cmd = connection.CreateCommand())
            {
                try
                {
                    foreach (DataTable table in this.Tables)
                    {
                        // テーブルの有無を確認
                        cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='" + table.TableName + "';";                       
                        using (SQLiteDataReader r = cmd.ExecuteReader())
                        {
                            if (r[0].ToString() != "0") continue;                           
                        }
 
                        // テーブルが存在しない場合はテーブル作成
                        StringBuilder commandBuilder = new StringBuilder();
                        commandBuilder.Append("CREATE TABLE " + table.TableName + " (");
                        for (int i = 0; i < table.Columns.Count; i++)
                        {
                            if (i != 0) commandBuilder.Append(",");
                            commandBuilder.Append(table.Columns[i].ColumnName);
                            if (!table.Columns[i].AllowDBNull) commandBuilder.Append(" NOT NULL");
                            if (Array.IndexOf(table.PrimaryKey, table.Columns[i]) >= 0) commandBuilder.Append(" PRIMARY KEY");
                            if (table.Columns[i].Unique) commandBuilder.Append(" UNIQUE");
                        }
                        commandBuilder.Append(")");

                        // テーブル作成
                        cmd.CommandText = commandBuilder.ToString();
                        cmd.ExecuteNonQuery();
                    }
                    tran.Commit(); // コミット
                }
                catch
                {
                    tran.Rollback(); // ロールバック
                    throw;
                }
            }
        }

        /// <summary>
        /// テーブルアダプタを作成する。
        /// </summary>
        /// <param name="table">アダプタを作成するテーブル。</param>
        /// <returns>データアダプタ。</returns>
        private static SQLiteDataAdapter CreateTableAdapter(DataTable table)
        {
            SQLiteDataAdapter adapter = new SQLiteDataAdapter("SELECT * FROM " + table.TableName, connection);
            SQLiteCommand insertCommand = new SQLiteCommand(connection);
            SQLiteCommand updateCommand = new SQLiteCommand(connection);
            SQLiteCommand deleteCommand = new SQLiteCommand(connection);
            StringBuilder colStrBuilder = new StringBuilder();
            StringBuilder valStrBuilder = new StringBuilder();

            // INSERT
            colStrBuilder.Clear();
            valStrBuilder.Clear();
            for (int i = 0; i < table.Columns.Count; i++)
            {
                System.Data.DataColumn col = table.Columns[i];
                colStrBuilder.Append((i != 0 ? "," : "") + col.ColumnName);
                valStrBuilder.Append((i != 0 ? "," : "") + "@" + col.ColumnName);

                SQLiteParameter param = new SQLiteParameter();
                param.ParameterName = "@" + col.ColumnName;
                param.SourceColumn = col.ColumnName;
                insertCommand.Parameters.Add(param);
            }
            insertCommand.CommandText = "INSERT OR REPLACE INTO " + table.TableName + "(" + colStrBuilder.ToString() + ") VALUES(" + valStrBuilder.ToString() + ")";
            adapter.InsertCommand = insertCommand;

            // UPDATE
            adapter.UpdateCommand = insertCommand; // INSERTと同じ

            // DELETE
            colStrBuilder.Clear();
            valStrBuilder.Clear();
            for (int i = 0; i < table.Columns.Count; i++)
            {
                System.Data.DataColumn col = table.Columns[i];
                if (Array.IndexOf(table.PrimaryKey, col) >= 0)
                {
                    colStrBuilder.Append((i != 0 ? " AND " : "") + col.ColumnName + "=@" + col.ColumnName);
                }

                SQLiteParameter param = new SQLiteParameter();
                param.ParameterName = "@" + col.ColumnName;
                param.SourceColumn = col.ColumnName;
                deleteCommand.Parameters.Add(param);
            }
            deleteCommand.CommandText = "DELETE FROM " + table.TableName + " WHERE " + colStrBuilder.ToString();
            adapter.DeleteCommand = deleteCommand;

            return adapter;
        }

        #endregion



        #region WebPage

        /// <summary>
        /// ウェブページのデータテーブル。
        /// </summary>
        partial class WebPageDataTable
        {
            /// <summary>テーブルアダプタ。</summary>
            public SQLiteDataAdapter Adapter { get; private set; }
            
            /// <summary>
            /// データベースのアダプタを作成する。
            /// </summary>
            /// <returns>テーブルアダプタ。</returns>
            public void CreateDataAdapter()
            {
                this.Adapter = WebPageDataSet.CreateTableAdapter(this);
            }

            /// <summary>
            /// データベースのテーブルを読み込む。
            /// </summary>
            public void FillTable()
            {
                Adapter.Fill(this);
            }

            /// <summary>
            /// データベースのテーブルを更新する。
            /// </summary>
            public void UpdateTable()
            {
                Adapter.Update(this);
            }

        }

        #endregion



        #region WebpageRow

        /// <summary>
        /// ウェブページのデータ行。
        /// </summary>
        partial class WebPageRow
        {
            public void SetUrl(string url)
            {
                if (url != this.Url)
                {
                    this.CheckedTime = DateTime.MinValue;
                    this.LatestTime = DateTime.MinValue;
                    this.LatestHtml = "";
                    this.LatestDiff = "";
                }
                this.Url = url;
            }



            /// <summary>
            /// ページの更新を確認する。
            /// 取得したページおよび差分内容はオブジェクトに保存される。
            /// </summary>
            /// <returns>変更があった場合はtrue。</returns>
            public bool CheckLatestPage()
            {
                // 更新確認しない場合
                if (!this.Status) return false;

                string diffStr = ""; // 変更結果文字

                try
                {
                    // HTMLを取得
                    string html = GetPageHtml(new Uri(this.Url));
                    if (html == null) throw new ApplicationException("ページが見つかりませんでした。");

                    // BODY要素を取得
                    Regex regexp = new Regex("<body[^>]*>(.*?)</body>",
                        RegexOptions.IgnoreCase | RegexOptions.Singleline);
                    Match match = regexp.Match(html);
                    if (!match.Success)
                    {
                        // bodyタグ閉じてないサイトがたまにある
                        regexp = new Regex("<body[^>]*>(.*?)$",
                        RegexOptions.IgnoreCase | RegexOptions.Singleline);
                        match = regexp.Match(html);
                    }

                    // 新テキスト
                    string newBody = WebUtility.HtmlDecode(match.Groups[1].Value);
                    string[] newBodyLines = newBody.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                    // 旧テキスト
                    string oldBody = this.LatestHtml;
                    string[] oldBodyLines = oldBody.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                    // 差分
                    DiffOption option = new DiffOption();
                    option.IgnoreSpace = Properties.Settings.Default.IgnoreSpace;
                    option.IgnoreCase = Properties.Settings.Default.IgnoreCase;
                    DiffResult[] diffs = FastDiff.Diff(this.LatestHtml, newBody, option);
                
                    // フィルタ条件を取得
                    int onStartIndex = newBody.IndexOf(FilterStart); if (onStartIndex < 0) onStartIndex = 0;
                    int onEndIndex = newBody.LastIndexOf(FilterEnd); if (onEndIndex < onStartIndex) onEndIndex = newBody.Length - 1;
                    string[] includes = this.FilterInclude.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                    string[] ignores = this.FilterIgnore.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);





                    bool onCheckArea = false;
                    for (int i = 0; i < diffs.Length; i++)
                    {
                        // 変更なしの場合は無視
                        if (!diffs[i].Modified)
                            continue;

                        // 変更前後の文字列取得
                        string newLine = newBodyLines[diffs[i].ModifiedStart];
                        string oldLine = oldBodyLines[diffs[i].OriginalStart];


                        // 変更箇所が範囲内に存在しない場合は対象外
                        if (diffs[i].ModifiedStart < onStartIndex ||
                            diffs[i].ModifiedStart + diffs[i].ModifiedLength > onEndIndex)
                        {
                            continue;
                        }

                        /*
                        // 開始位置検出
                        if (onCheckArea == false)
                        {
                            if (this.FilterStart != "" && newLine.IndexOf(this.FilterStart) < 0)
                                continue;
                            else
                                onCheckArea = true;

                        }
                        // 終了位置検出
                        if (onCheckArea == true)
                        {
                            if (this.FilterEnd != "" && newLine.IndexOf(this.FilterEnd) > 0)
                                break;
                        }
                        */


                        // 差分行に対象文字列が含まれているか判定
                        // 未設定の場合は何もしない
                        bool found = false;
                        if (includes.Length > 0)
                        {
                            foreach (string include in includes)
                            {
                                // Regex reg = new Regex(include, RegexOptions.IgnoreCase | RegexOptions.Singleline);
                                //Match m = regexp.Match(newLine);

                                //if (m.Success)
                                if (newLine.IndexOf(include) >= 0)
                                {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found) continue; // この差分行は対象外
                        }

                        // 差分行に無視文字列が含まれているか判定
                        // 未設定の場合は何もしない
                        if (ignores.Length > 0)
                        {
                            foreach (string ignore in ignores)
                            {
                                //Regex reg = new Regex(ignore,
                                //    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                                //Match m = regexp.Match(newLine);

                                //if (m.Success)
                                if (newLine.IndexOf(ignore) >= 0)
                                {
                                    found = true;
                                    break;
                                }
                            }
                            if (found) continue; // この差分行は対象外
                        }

                        // 変更差分を保存
                        diffStr +=
                            oldLine + Environment.NewLine +
                            "----------------------------------------" + Environment.NewLine +
                            newLine + Environment.NewLine +
                            Environment.NewLine +
                            "========================================" + Environment.NewLine +
                            Environment.NewLine;

                    }

                    this.LatestHtml = newBody;
                    this.LatestDiff = diffStr;
                }
                catch
                {
                }

                // 更新チェック日時を設定
                this.CheckedTime = DateTime.Now;

                
                // 変更検出した場合は更新情報を設定してtrueを返す
                if (diffStr != "")
                {
                    this.LatestTime = this.CheckedTime;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        #endregion



        #region Settings

        /// <summary>
        /// 設定データテーブル。
        /// </summary>
        partial class SettingsDataTable
        {
            /// <summary>テーブルアダプタ。</summary>
            public SQLiteDataAdapter Adapter { get; private set; }

            /// <summary>
            /// データベースのアダプタを作成する。
            /// </summary>
            /// <returns>テーブルアダプタ。</returns>
            public void CreateDataAdapter()
            {
                this.Adapter = WebPageDataSet.CreateTableAdapter(this);
            }


            /// <summary>
            /// データベースのテーブルを設定に読み込む。
            /// </summary>
            public void FillTable()
            {
                Adapter.Fill(this);

                using (SQLiteTransaction tran = connection.BeginTransaction())
                {
                    try
                    {
                        foreach (SettingsPropertyValue propertyValue in Properties.Settings.Default.PropertyValues)
                        {
                            string name = propertyValue.Name;

                            // テーブルを読み込む
                            SettingsRow row = this.FindByName(name);
                            if (row == null) continue;

                            string value = row[this.ValueColumn].ToString();
                            // パスワードを復号
                            if (name == "SenderPassword")
                                value = DecryptString(value);

                            // 設定を読込み
                            propertyValue.SerializedValue = value;
                        }

                        tran.Commit(); // コミット
                    }
                    catch
                    {
                        tran.Rollback(); // ロールバック
                        throw;
                    }
                }
            }

            /// <summary>
            /// 設定をデータベースのテーブルに書き込む。
            /// </summary>
            public void UpdateTable()
            {
                using (SQLiteTransaction tran = connection.BeginTransaction())
                {
                    try
                    {
                        foreach (SettingsPropertyValue propertyValue in Properties.Settings.Default.PropertyValues)
                        {
                            string name = propertyValue.Name;
                            string value = propertyValue.SerializedValue.ToString();

                            // パスワードを暗号化
                            if (name == "SenderPassword")
                                value = EncryptString(value);
 
                            SettingsRow row = this.FindByName(name);
                            if (row != null)
                            {
                                // 既存値の場合
                                if (!row[this.ValueColumn].Equals(value))
                                {
                                    // 変更が無い場合は何もしない
                                    row[this.ValueColumn] = value;
                                }
                            }
                            else
                            {
                                // 新規値の場合
                                SettingsRow newRow = this.NewSettingsRow();
                                newRow.Name = name;
                                newRow.Value = value;
                                this.Rows.Add(newRow);
                            }
                        }

                        tran.Commit(); // コミット
                    }
                    catch
                    {
                        tran.Rollback(); // ロールバック
                        throw;
                    }
                }

                Adapter.Update(this);
            }

            /// <summary>
            /// 文字列を暗号化する。
            /// </summary>
            /// <param name="text">暗号化する平文。</param>
            /// <returns>暗号文。</returns>
            private string EncryptString(string text)
            {
                try
                {
                    // 暗号用追加パラメータ
                    byte[] entropy = { 0x77, 0x73, 0x75, 0x63 };

                    //文字列をバイト型配列に変換
                    byte[] textBytes = Encoding.UTF8.GetBytes(text);

                    // 暗号化
                    byte[] encryptedData = ProtectedData.Protect(textBytes, entropy, DataProtectionScope.CurrentUser);

                    // 暗号化データを文字列に変換
                    return Convert.ToBase64String(encryptedData);
                }
                catch
                {
                    // 暗号化に失敗した場合は空文字を返す
                    return "";
                }
            }

            /// <summary>
            /// 文字列を復号する。
            /// </summary>
            /// <param name="text">復号する暗号文。</param>
            /// <returns>平文</returns>
            private string DecryptString(string text)
            {
                try
                {
                    // 暗号用追加パラメータ
                    byte[] entropy = { 0x77, 0x73, 0x75, 0x63 };

                    // 文字列を暗号データに戻す
                    byte[] encryptedData = Convert.FromBase64String(text);

                    //復号
                    byte[] textBytes = ProtectedData.Unprotect(encryptedData, entropy, DataProtectionScope.CurrentUser);

                    //復号化されたデータを文字列に変換
                    return Encoding.UTF8.GetString(textBytes);
                }
                catch
                {
                    // 復号に失敗した場合は空文字を返す
                    return "";
                }
            }

        }

        #endregion



        #region ページ文字列処理

        /// <summary>
        /// 指定したURLのページのHTML文字列を取得する。
        /// HTMLエンコード文字はデコードされない。
        /// 文字コードは自動判別、改行コードは全て\r\nに変換される。
        /// </summary>
        /// <param name="url">取得対象ページのURL。</param>
        /// <returns>ページのHTML。</returns>
        public static string GetPageHtml(Uri url)
        {
            HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url);
            req.AllowAutoRedirect = true;
            req.Timeout = 10000;

            byte[] pageBytes;
            Encoding pageEncoding;

            try
            {
                using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
                using (Stream stream = res.GetResponseStream())
                using (MemoryStream ms = new MemoryStream())
                {
                    int readedLength = 0;
                    byte[] readBytes = new byte[256];
                    do
                    {
                        // ストリームからバッファの読み込み
                        readedLength = stream.Read(readBytes, 0, readBytes.Length);
                        // バッファをメモリストリームへ書き込み
                        ms.Write(readBytes, 0, readedLength);

                    } while (readedLength > 0);

                    // ページ取得
                    pageBytes = ms.ToArray();
                    // 文字コード取得
                    pageEncoding = GetCode(pageBytes);
                    //try
                    //{
                    //    pageEncoding = Encoding.GetEncoding(res.CharacterSet);
                    //}
                    //catch
                    //{
                    //    pageEncoding = GetCode(pageBytes);
                    //}                
                }


                // 改行コードを統一して文字列取得
                StringBuilder sb = new StringBuilder("");
                using (StreamReader sr = new StreamReader(new MemoryStream(pageBytes), pageEncoding))
                {
                    string line;
                    while ((line = sr.ReadLine()) != null)
                        sb.AppendLine(line);
                }

                return sb.ToString();

            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// 文字コードを判別する
        /// </summary>
        /// <remarks>
        /// Jcode.pmのgetcodeメソッドを移植したものです。
        /// Jcode.pm(http://openlab.ring.gr.jp/Jcode/index-j.html)
        /// Jcode.pmのCopyright: Copyright 1999-2005 Dan Kogai
        /// </remarks>
        /// <param name="bytes">文字コードを調べるデータ</param>
        /// <returns>適当と思われるEncodingオブジェクト。
        /// 判断できなかった時はnull。</returns>
        private static Encoding GetCode(byte[] bytes)
        {
            const byte bEscape = 0x1B;
            const byte bAt = 0x40;
            const byte bDollar = 0x24;
            const byte bAnd = 0x26;
            const byte bOpen = 0x28;    //'('
            const byte bB = 0x42;
            const byte bD = 0x44;
            const byte bJ = 0x4A;
            const byte bI = 0x49;

            int len = bytes.Length;
            byte b1, b2, b3, b4;

            //Encode::is_utf8 は無視

            bool isBinary = false;
            for (int i = 0; i < len; i++)
            {
                b1 = bytes[i];
                if (b1 <= 0x06 || b1 == 0x7F || b1 == 0xFF)
                {
                    //'binary'
                    isBinary = true;
                    if (b1 == 0x00 && i < len - 1 && bytes[i + 1] <= 0x7F)
                    {
                        //smells like raw unicode
                        return System.Text.Encoding.Unicode;
                    }
                }
            }
            if (isBinary)
            {
                return null;
            }

            //not Japanese
            bool notJapanese = true;
            for (int i = 0; i < len; i++)
            {
                b1 = bytes[i];
                if (b1 == bEscape || 0x80 <= b1)
                {
                    notJapanese = false;
                    break;
                }
            }
            if (notJapanese)
            {
                return System.Text.Encoding.ASCII;
            }

            for (int i = 0; i < len - 2; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                b3 = bytes[i + 2];

                if (b1 == bEscape)
                {
                    if (b2 == bDollar && b3 == bAt)
                    {
                        //JIS_0208 1978
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bDollar && b3 == bB)
                    {
                        //JIS_0208 1983
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bOpen && (b3 == bB || b3 == bJ))
                    {
                        //JIS_ASC
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bOpen && b3 == bI)
                    {
                        //JIS_KANA
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    if (i < len - 3)
                    {
                        b4 = bytes[i + 3];
                        if (b2 == bDollar && b3 == bOpen && b4 == bD)
                        {
                            //JIS_0212
                            //JIS
                            return System.Text.Encoding.GetEncoding(50220);
                        }
                        if (i < len - 5 &&
                            b2 == bAnd && b3 == bAt && b4 == bEscape &&
                            bytes[i + 4] == bDollar && bytes[i + 5] == bB)
                        {
                            //JIS_0208 1990
                            //JIS
                            return System.Text.Encoding.GetEncoding(50220);
                        }
                    }
                }
            }

            //should be euc|sjis|utf8
            //use of (?:) by Hiroki Ohzaki <ohzaki@iod.ricoh.co.jp>
            int sjis = 0;
            int euc = 0;
            int utf8 = 0;
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if (((0x81 <= b1 && b1 <= 0x9F) || (0xE0 <= b1 && b1 <= 0xFC)) &&
                    ((0x40 <= b2 && b2 <= 0x7E) || (0x80 <= b2 && b2 <= 0xFC)))
                {
                    //SJIS_C
                    sjis += 2;
                    i++;
                }
            }
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if (((0xA1 <= b1 && b1 <= 0xFE) && (0xA1 <= b2 && b2 <= 0xFE)) ||
                    (b1 == 0x8E && (0xA1 <= b2 && b2 <= 0xDF)))
                {
                    //EUC_C
                    //EUC_KANA
                    euc += 2;
                    i++;
                }
                else if (i < len - 2)
                {
                    b3 = bytes[i + 2];
                    if (b1 == 0x8F && (0xA1 <= b2 && b2 <= 0xFE) &&
                        (0xA1 <= b3 && b3 <= 0xFE))
                    {
                        //EUC_0212
                        euc += 3;
                        i += 2;
                    }
                }
            }
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if ((0xC0 <= b1 && b1 <= 0xDF) && (0x80 <= b2 && b2 <= 0xBF))
                {
                    //UTF8
                    utf8 += 2;
                    i++;
                }
                else if (i < len - 2)
                {
                    b3 = bytes[i + 2];
                    if ((0xE0 <= b1 && b1 <= 0xEF) && (0x80 <= b2 && b2 <= 0xBF) &&
                        (0x80 <= b3 && b3 <= 0xBF))
                    {
                        //UTF8
                        utf8 += 3;
                        i += 2;
                    }
                }
            }
            //M. Takahashi's suggestion
            //utf8 += utf8 / 2;

            System.Diagnostics.Debug.WriteLine(
                string.Format("sjis = {0}, euc = {1}, utf8 = {2}", sjis, euc, utf8));
            if (euc > sjis && euc > utf8)
            {
                //EUC
                return System.Text.Encoding.GetEncoding(51932);
            }
            else if (sjis > euc && sjis > utf8)
            {
                //SJIS
                return System.Text.Encoding.GetEncoding(932);
            }
            else if (utf8 > euc && utf8 > sjis)
            {
                //UTF8
                return System.Text.Encoding.UTF8;
            }

            return null;
        }

        #endregion

    }
}


